//
// Unihan Input Method
//
// Copyright © 2002-2007 Sun Wah Linux Ltd.
//
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
// USA
//
// $Id:$
//

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <unihan_im.h>
#include <unihan_im_keycodes.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <m17n.h>

#ifndef _DEBUG_LOG
#ifdef DEBUG
#define _DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__);
#else
#define _DEBUG_LOG(...)
#endif
#endif

typedef struct {
	char *lang;
	char *name;
} M17NIMInfo;

typedef struct {
	MInputMethod *mim;
} M17NIMDesktopData;

typedef struct {
	char preedit[7];
	int len;
	int pos;

	UnihanIM *uim;
	MInputContext *mic;
} M17NIMSessionData;

typedef struct _M17NIMSessionDataList {
	M17NIMSessionData *ds;
	struct _M17NIMSessionDataList *next;
} M17NIMSessionDataList;

int unihan_im_module_init(void);
void unihan_im_module_done(void);
int unihan_im_module_get_info(UnihanIMModuleArgList *args);
static IMInfo *m17nim_get_im_list(void);

static int m17nim_init(UnihanIM *uim);
static int m17nim_done(UnihanIM *uim);
static int m17nim_create_session(UnihanIM *uim);
static int m17nim_destroy_session(UnihanIM *uim);
static int m17nim_handle_event(UnihanIM *uim, IMEvent *ev);
static int handle_key_event(UnihanIM *uim, IMKeyEvent *ev);
static int handle_focus_event(UnihanIM *uim, IMFocusEvent *ev);
static int handle_config_change_event(UnihanIM *uim, IMConfigChangeEvent *ev);
static int handle_switch_event(UnihanIM *uim, IMSwitchEvent *ev);
static unsigned char *ucs4_to_utf8(UnihanIM *uim, unsigned long int ucs4);
static const unsigned char *utf8_next_char(const unsigned char *in);
static void preedit_start_cb(MInputContext *mic, MSymbol command);
static void preedit_draw_cb(MInputContext *mic, MSymbol command);
static void preedit_done_cb(MInputContext *mic, MSymbol command);
static void status_start_cb(MInputContext *mic, MSymbol command);
static void status_draw_cb(MInputContext *mic, MSymbol command);
static void status_done_cb(MInputContext *mic, MSymbol command);
static void candidates_start_cb(MInputContext *mic, MSymbol command);
static void candidates_draw_cb(MInputContext *mic, MSymbol command);
static void candidates_done_cb(MInputContext *mic, MSymbol command);
static MSymbol keyevent_to_msymbol(IMKeyEvent *ev);

static MConverter *__m17n_converter = NULL;
static M17NIMSessionDataList *__m17n_session_data_list = NULL;

int
unihan_im_module_init(void)
{
	_DEBUG_LOG("m17nim: unihan_im_module_init()\n");

	/* The input module can do some initialization here */

	M17N_INIT();

	__m17n_converter = mconv_buffer_converter(msymbol("utf8"), NULL, 0);
	if (!__m17n_converter)
		return FALSE;
	
	return TRUE;
}


void
unihan_im_module_done(void)
{
	_DEBUG_LOG("m17nim: unihan_im_module_done()\n");

	/* The input module can do some finalization here */

	M17N_FINI();
	
	return;
}


int
unihan_im_module_get_info(UnihanIMModuleArgList *args)
{
	_DEBUG_LOG("m17nim: unihan_im_module_get_info()\n");
	
	if (!args) return FALSE;
	while (args->id) {
		switch (args->id) {
			case UNIHAN_IM_API_VERSION:
				/* Unihan only supports API version 1.0 now */
				args->value = (void*)0x00010000;
				break;
			case UNIHAN_IM_MODULE_ID:
				/* The ID of the input module */
				args->value = strdup("m17nim");
				break;
			case UNIHAN_IM_MODULE_NAME:
				/* Human readable name of the input module */
				args->value = strdup("m17nlib input method");
				break;
			case UNIHAN_IM_MODULE_VERSION:
				/* Version of the input module */
				args->value = strdup("1.0");
				break;
			case UNIHAN_IM_MODULE_IM_LIST:
				args->value = m17nim_get_im_list();
				break;
		}
		++args;
	}

	return TRUE;
}


static IMInfo *
m17nim_get_im_list(void)
{
	IMInfo *im_list = NULL;

	MPlist *foo, *mimlist = mdatabase_list(msymbol("input-method"), Mnil, Mnil, Mnil);
	for (foo = mimlist; foo && mplist_key(foo) != Mnil; foo = mplist_next(foo)) {
		MDatabase *mdb = (MDatabase *)mplist_value(foo);
		MSymbol *tag = mdatabase_tag(mdb);
		if (Mnil != tag[1]) {
			MInputMethod *mim = minput_open_im(tag[1], tag[2], NULL);
			if (mim) {
				const char *lang = msymbol_name(mim->language);
				const char *name = msymbol_name(mim->name);
				if (lang && lang[0] && name && name[0]) {
					M17NIMInfo *miminfo = (M17NIMInfo *)malloc(sizeof(M17NIMInfo));
					miminfo->lang = strdup(msymbol_name(mim->language));
					miminfo->name = strdup(msymbol_name(mim->name));

					IMInfo *iminfo = (IMInfo *)malloc(sizeof(IMInfo));
					memset(iminfo, 0, sizeof(IMInfo));

					IMMethods *imm = (IMMethods *)malloc(sizeof(IMMethods));
					memset(imm, 0, sizeof(IMMethods));
					imm->init = m17nim_init;
					imm->done = m17nim_done;
					imm->create_session = m17nim_create_session;
					imm->destroy_session = m17nim_destroy_session;
					imm->handle_event = m17nim_handle_event;

                    MPlist *mimtitle = minput_get_title_icon(mim->language, mim->name);
                    if (mimtitle) {
                        MText *title = (MText *)mplist_value(mimtitle);
                        char buf[1024];
                        mconv_rebind_buffer(__m17n_converter, (unsigned char *)buf, 1024);
                        mconv_encode(__m17n_converter, title);
                        buf[__m17n_converter->nbytes] = 0;
                        m17n_object_unref(mimtitle);
                        strcat(buf, " (m17n)");
                        iminfo->name = strdup(buf);
                    } else {
                        iminfo->name = malloc(256);
                        snprintf(iminfo->name, 255, "m17n-%s-%s", lang, name);
                    }
					iminfo->id = malloc(256);
					snprintf(iminfo->id, 255, "m17n-%s-%s", lang, name);
					iminfo->version = strdup("1.0");
					iminfo->methodTable = imm;
					iminfo->imUserData = miminfo;
					iminfo->next = im_list;
					im_list = iminfo;
				}
				minput_close_im(mim);
			}
		}
	}
	m17n_object_unref(mimlist);

	return im_list;
}


static int
m17nim_init(UnihanIM *uim)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "m17nim_init()");

	M17NIMDesktopData *dd = malloc(sizeof(M17NIMDesktopData));
	memset(dd, 0, sizeof(M17NIMDesktopData));

	M17NIMInfo *miminfo = (M17NIMInfo *)uim->imUserData;

	dd->mim = minput_open_im(msymbol(miminfo->lang), msymbol(miminfo->name), NULL);

	if (dd->mim) {
		mplist_add(dd->mim->driver.callback_list, Minput_preedit_start,    (void *)preedit_start_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_preedit_draw,     (void *)preedit_draw_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_preedit_done,     (void *)preedit_done_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_status_start,     (void *)status_start_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_status_draw,      (void *)status_draw_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_status_done,      (void *)status_done_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_candidates_start, (void *)candidates_start_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_candidates_draw,  (void *)candidates_draw_cb);
		mplist_add(dd->mim->driver.callback_list, Minput_candidates_done,  (void *)candidates_done_cb);

        // MPlist configlist = mplist();
        // mplist_add(configlist, Minteger, (void*)9);
        // minput_config_variable(msymbol(miminfo->lang), msymbol(miminfo->name), msymbol("candidates-group-size"), configlist);

		uim->desktopUserData = dd;
		return TRUE;
	}

	free(dd);
	return FALSE;
}


static int
m17nim_done(UnihanIM *uim)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "m17nim_done()");

	M17NIMDesktopData *dd = (M17NIMDesktopData *)uim->desktopUserData;
	minput_close_im(dd->mim);
	return TRUE;
}


static int
m17nim_create_session(UnihanIM *uim)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "m17nim_create_session()");

	M17NIMDesktopData *dd = (M17NIMDesktopData *)uim->desktopUserData;

	M17NIMSessionData *ds = malloc(sizeof(M17NIMSessionData));
	memset(ds, 0, sizeof(M17NIMSessionData));
	ds->uim = uim;

	ds->mic = minput_create_ic(dd->mim, NULL);
	if (ds->mic) {
		M17NIMSessionDataList *l = malloc(sizeof(M17NIMSessionDataList));
		l->ds = ds;
		l->next = __m17n_session_data_list;
		__m17n_session_data_list = l;

		uim->sessionUserData = ds;
		return TRUE;
	}
	free(ds);
	return FALSE;
}


static int
m17nim_destroy_session(UnihanIM *uim)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "m17nim_destroy_session()");

	M17NIMSessionData *ds = (M17NIMSessionData *)uim->sessionUserData;
	ds->uim = uim;
	minput_destroy_ic(ds->mic);

	M17NIMSessionDataList *prev = NULL, *i = __m17n_session_data_list;
	while (i) {
		if (i->ds == ds) {
			if (prev)
				prev->next = i->next;
			else 
				__m17n_session_data_list = i->next;
			free(i);
			break;
		}
		prev = i;
		i = i->next;
	}

	free(ds);
	return TRUE;
}


static int
m17nim_handle_event(UnihanIM *uim, IMEvent *ev)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "m17nim_handle_event()");

	// m17nlib callback functions only have MInputContext as identifier.
	// Therefore we need to save the uim pointer somewhere
	// so that the callbacks from m17nlib can retrieve it.
	M17NIMSessionData *ds = (M17NIMSessionData *)uim->sessionUserData;
	ds->uim = uim;

	switch (ev->type) {
		case IM_KEY:
			return handle_key_event(uim, (IMKeyEvent*)ev);
		case IM_FOCUS:
			return handle_focus_event(uim, (IMFocusEvent*)ev);
		case IM_CONFIG_CHANGE:
			return handle_config_change_event(uim, (IMConfigChangeEvent*)ev);
		case IM_SWITCH:
			return handle_switch_event(uim, (IMSwitchEvent*)ev);
        case IM_LOOKUP_PAGE_TURN:
        {
            // we are abusing key handling again...
            IMKeyEvent kev;
            kev.type = IM_KEY;
            kev.keyChar = 0;
            kev.modifier = 0;
            kev.keyCode = (ev->lookupPageTurn.direction < 0)? IM_VK_UP : IM_VK_DOWN;
            return handle_key_event(uim, &kev);
        }
        case IM_LOOKUP_SELECT:
        {
            if (ev->lookupSelect.num < 0 || ev->lookupSelect.num > 9)
                return FALSE;

            IMKeyEvent kev;
            kev.type = IM_KEY;
            kev.modifier = 0;
            if (ev->lookupSelect.num == 9) {
                kev.keyChar = '0';
                kev.keyCode = IM_VK_0;
            } else {
                kev.keyChar = ev->lookupSelect.num + '1';
                kev.keyCode = ev->lookupSelect.num + IM_VK_1;
            }
            return handle_key_event(uim, &kev);
        }
		case IM_NOTHING:
		default:
			break;
	}

	return FALSE;
}


static int
handle_key_event(UnihanIM *uim, IMKeyEvent *ev)
{
	M17NIMSessionData *ds = (M17NIMSessionData *)uim->sessionUserData;

	MSymbol mkey = keyevent_to_msymbol(ev);
	if (mkey == Mnil) {
		uim->m->log(uim, UNIHAN_LOG_LEVEL_WARNING, "could not convert keyevent to MSymbol");
		return FALSE;
	}
    uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "handling key %s", msymbol_name(mkey));

	int ret = minput_filter(ds->mic, mkey, NULL);
	if (ret == 1) {
		uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "minput_filter returns 1");
		return TRUE;
	}

	MText *produced = mtext();
	ret = minput_lookup(ds->mic, mkey, NULL, produced);
    if (mtext_len(produced) <= 0) {
        uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "minput_lookup returns -1");
        return FALSE;
    }

	char buf[1024];
	mconv_rebind_buffer(__m17n_converter, (unsigned char *)buf, 1024);
	mconv_encode(__m17n_converter, produced);
	buf[__m17n_converter->nbytes] = 0;
	m17n_object_unref(produced);

	if (strlen(buf)) {
		uim->m->commit_string(uim, buf);
	}

	return (ret == 0);
}


static int
handle_focus_event(UnihanIM *uim, IMFocusEvent *ev)
{
    uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "focus %s", (ev->focused)? "in" : "out");
	M17NIMSessionData *ds = (M17NIMSessionData *)uim->sessionUserData;
	if (ev->focused == TRUE) {
		/* the input area has received focus */
		minput_filter(ds->mic, Minput_focus_in, NULL);
        preedit_draw_cb(ds->mic, Minput_preedit_draw);
        candidates_draw_cb(ds->mic, Minput_candidates_draw);
	} else {
		/* the input area lost focus */
		minput_filter(ds->mic, Minput_focus_out, NULL);
        preedit_done_cb(ds->mic, Minput_preedit_done);
        candidates_done_cb(ds->mic, Minput_candidates_done);
	}
    return TRUE;
}


static int
handle_config_change_event(UnihanIM *uim, IMConfigChangeEvent *ev)
{
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "config key %s has changed", ev->key);
    return FALSE;
}


static int
handle_switch_event(UnihanIM *uim, IMSwitchEvent *ev)
{
    uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "switch %s", (ev->on)? "on" : "off");
	M17NIMSessionData *ds = (M17NIMSessionData *)uim->sessionUserData;
	if (ev->on == TRUE) {
		/* the user has switched on this input method */
	} else {
		/* the user has switched off this input method */
        minput_reset_ic(ds->mic);
	}
    uim->m->preedit_erase(uim);
    uim->m->lookup_done(uim);
    return TRUE;
}


static UnihanIM *
find_uim_from_mic(MInputContext *mic)
{
	M17NIMSessionDataList *i = __m17n_session_data_list;
	while (i) {
		if (i->ds->mic == mic)
			return i->ds->uim;
		i = i->next;
	}
	return NULL;
}


static void
preedit_start_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

	// actually preedit_start in Unihan is a no-op
}


static void
preedit_draw_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

	char buf[1024];
	mconv_rebind_buffer(__m17n_converter, (unsigned char *)buf, 1024);
	mconv_encode(__m17n_converter, mic->preedit);
	buf[__m17n_converter->nbytes] = 0;

	// FIXME: styles
	
	if (strlen(buf) == 0) {
		uim->m->preedit_erase(uim);
        return;
    }

    uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "preedit_draw, cursor_pos %d", mic->cursor_pos);
    uim->m->preedit_draw(uim, buf, NULL);
    uim->m->preedit_caret_pos(uim, mic->cursor_pos);
}


static void
preedit_done_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

	uim->m->preedit_erase(uim);
}

static void
status_start_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;
}


static void
status_draw_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;
}


static void
status_done_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;
}


static void
candidates_start_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

    UnihanLookupStart lustart;
    lustart.choicePerWindow = 10;
    lustart.numColumns = 10;
    lustart.numRows = 1;
    lustart.direction = UNIHAN_LOOKUP_DIRECTION_RIGHT;
    uim->m->lookup_start(uim, &lustart);
}


static void
candidates_draw_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "candidates_draw_cb: ");
	uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "index = %d, from = %d, to = %d, show = %d\n",
			mic->candidate_index,
			mic->candidate_from,
			mic->candidate_to,
			mic->candidate_show);

    if (!mic->candidate_list || !mic->candidate_show) {
        candidates_done_cb(mic, command);
        return;
    }

    int total = 0, offset = 0;
    MPlist *curgroup, *group = mic->candidate_list;
    while (group && mplist_key(group) != Mnil) {
        int len = 0;
        if (mplist_key(group) == Mtext)
            len = mtext_len(mplist_value(group));
        else
            len = mplist_length(mplist_value(group));
        if (total <= mic->candidate_index) {
            offset = total;
            curgroup = group;
        }
        total += len;
        group = mplist_next(group);
    }
    int cur = mic->candidate_index - offset;

    UnihanLookupDraw ludraw;
    ludraw.mode = UNIHAN_LOOKUP_MODE_NORMAL;
    ludraw.selected = cur;
    ludraw.currentChoice = mic->candidate_index;
    ludraw.totalChoices = total;
    ludraw.numChoices = total - offset;
    if (ludraw.numChoices > 10)
        ludraw.numChoices = 10;

    uim->m->mem_alloc(uim, sizeof(char *) * ludraw.numChoices, (void**)&(ludraw.label));
    uim->m->mem_alloc(uim, sizeof(char *) * ludraw.numChoices, (void**)&(ludraw.value));

    group = curgroup;
    if (mplist_key(group) == Mtext) {
        uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "%d candidates in MText", total-offset);
        // the group is a string of MText, 1 char per candidate
        MText *mt = (MText *)mplist_value(group);
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        mconv_rebind_buffer(__m17n_converter, (unsigned char *)buf, sizeof(buf));
        mconv_encode(__m17n_converter, mt);

        int i = 0;
        const char *next, *c = buf;
        while (i < ludraw.numChoices && c && *c) {
            next = utf8_next_char(c);
            int nbytes = next - c;
            uim->m->mem_alloc(uim, nbytes + 1, (void**)&(ludraw.value[i]));
            memset(ludraw.value[i], 0, nbytes + 1);
            strncpy(ludraw.value[i], c, nbytes);
            c = next;
            ++i;
        }
    } else {
        uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "%d candidates in MPlist", total-offset);
        // an MPlist of candidate
        MPlist *pl;
        int i = 0;
        for (pl = (MPlist *)mplist_value(group); i < ludraw.numChoices &&  mplist_key(pl) != Mnil; pl = mplist_next(pl), ++i) {
            MText *mt = (MText *)mplist_value(pl);
            char buf[1024];
            memset(buf, 0, sizeof(buf));
            mconv_rebind_buffer(__m17n_converter, (unsigned char *)buf, sizeof(buf));
            mconv_encode(__m17n_converter, mt);
            buf[__m17n_converter->nbytes] = 0;
            uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "candidate %d, %d bytes", i, __m17n_converter->nbytes);

            if (buf[0] != '\0') {
                uim->m->mem_alloc(uim, __m17n_converter->nbytes + 1, (void**)&(ludraw.value[i]));
                memset(ludraw.value[i], 0, __m17n_converter->nbytes + 1);
                strncpy(ludraw.value[i], buf, __m17n_converter->nbytes);
            }
        }
    }

    int i;
    for (i = 0; i < ludraw.numChoices; ++i) {
        uim->m->mem_alloc(uim, sizeof(char)*2, (void**)&(ludraw.label[i]));
        ludraw.label[i][0] = i + '1';
        ludraw.label[i][1] = 0;
    }
    if (ludraw.numChoices == 10)
        ludraw.label[9][0] = '0';
    ludraw.labelStyles = NULL;
    ludraw.valueStyles = NULL;

    uim->m->lookup_draw(uim, &ludraw);
}


static void
candidates_done_cb(MInputContext *mic, MSymbol command)
{
	UnihanIM *uim = find_uim_from_mic(mic);
	if (!uim) return;

	uim->m->lookup_done(uim);
}


static unsigned char *
ucs4_to_utf8(UnihanIM *uim, unsigned long int ucs4)
{
	int len = 0, byte1, i;
    unsigned char *utf8 = NULL;

	if (ucs4 < 0x80) {
		byte1 = 0;
		len = 1;
	} else if (ucs4 < 0x800) {
		byte1 = 0xC0;
		len = 2;
	} else if (ucs4 < 0x10000) {
		byte1 = 0xE0;
		len = 3;
	} else if (ucs4 < 0x200000) {
		byte1 = 0xF0;
		len = 4;
	} else if (ucs4 < 0x4000000) {
		byte1 = 0xF8;
		len = 5;
	} else {
		byte1 = 0xFC;
		len = 6;
	}

    uim->m->mem_alloc(uim, len + 1, (void **)&utf8);
	if (utf8) {
        memset(utf8, 0, len + 1);
		for (i = len - 1; i > 0; --i) {
			utf8[i] = (ucs4 & 0x3F) | 0x80;
			ucs4 >>= 6;
		}
		utf8[0] = ucs4 | byte1;
	}

#if 1
    uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "converted from UCS-4 to UTF-8: ");
    for (i = 0; utf8[i]; ++i) {
        uim->m->log(uim, UNIHAN_LOG_LEVEL_DEBUG, "%02x ", utf8[i]);
    }
#endif

	return utf8;
}


typedef struct {
    short keycode;
    char *name;
} keycode_name_pair_t;

static keycode_name_pair_t keycode_name_pairs[] = {
    { IM_VK_ENTER,         "Return"},
    { IM_VK_BACK_SPACE,    "BackSpace"},
    { IM_VK_TAB,           "Tab"},
    { IM_VK_CANCEL,        "Cancel"},
    { IM_VK_CLEAR,         "Clear"},
    { IM_VK_SHIFT,         "Shift"},
    { IM_VK_CONTROL,       "Control"},
    { IM_VK_ALT,           "Alt"},
    { IM_VK_PAUSE,         "Pause"},
    { IM_VK_CAPS_LOCK,     "Caps_Lock"},
    { IM_VK_ESCAPE,        "Escape"},
    { IM_VK_SPACE,         "Space"},
    { IM_VK_PAGE_UP,       "Prior"},
    { IM_VK_PAGE_DOWN,     "Next"},
    { IM_VK_END,           "End"},
    { IM_VK_HOME,          "Home"},
    { IM_VK_LEFT,          "Left"},
    { IM_VK_UP,            "Up"},
    { IM_VK_RIGHT,         "Right"},
    { IM_VK_DOWN,          "Down"},
    { IM_VK_COMMA,         "comma"},
    { IM_VK_MINUS,         "minus"},
    { IM_VK_PERIOD,        "period"},
    { IM_VK_SLASH,         "slash"},
    { IM_VK_0,             "0"},
    { IM_VK_1,             "1"},
    { IM_VK_2,             "2"},
    { IM_VK_3,             "3"},
    { IM_VK_4,             "4"},
    { IM_VK_5,             "5"},
    { IM_VK_6,             "6"},
    { IM_VK_7,             "7"},
    { IM_VK_8,             "8"},
    { IM_VK_9,             "9"},
    { IM_VK_SEMICOLON,     "semicolon"},
    { IM_VK_EQUALS,        "equal"},
    { IM_VK_A,             "A"},
    { IM_VK_B,             "B"},
    { IM_VK_C,             "C"},
    { IM_VK_D,             "D"},
    { IM_VK_E,             "E"},
    { IM_VK_F,             "F"},
    { IM_VK_G,             "G"},
    { IM_VK_H,             "H"},
    { IM_VK_I,             "I"},
    { IM_VK_J,             "J"},
    { IM_VK_K,             "K"},
    { IM_VK_L,             "L"},
    { IM_VK_M,             "M"},
    { IM_VK_N,             "N"},
    { IM_VK_O,             "O"},
    { IM_VK_P,             "P"},
    { IM_VK_Q,             "Q"},
    { IM_VK_R,             "R"},
    { IM_VK_S,             "S"},
    { IM_VK_T,             "T"},
    { IM_VK_U,             "U"},
    { IM_VK_V,             "V"},
    { IM_VK_W,             "W"},
    { IM_VK_X,             "X"},
    { IM_VK_Y,             "Y"},
    { IM_VK_Z,             "Z"},
    { IM_VK_OPEN_BRACKET,  "bracketleft"},
    { IM_VK_BACK_SLASH,    "backslash"},
    { IM_VK_CLOSE_BRACKET, "bracketright"},
    { IM_VK_NUMPAD0,       "KP_0"},
    { IM_VK_NUMPAD1,       "KP_1"},
    { IM_VK_NUMPAD2,       "KP_2"},
    { IM_VK_NUMPAD3,       "KP_3"},
    { IM_VK_NUMPAD4,       "KP_4"},
    { IM_VK_NUMPAD5,       "KP_5"},
    { IM_VK_NUMPAD6,       "KP_6"},
    { IM_VK_NUMPAD7,       "KP_7"},
    { IM_VK_NUMPAD8,       "KP_8"},
    { IM_VK_NUMPAD9,       "KP_9"},
    { IM_VK_MULTIPLY,      "KP_Multiply"},
    { IM_VK_ADD,           "KP_Add"},
    { IM_VK_SUBTRACT,      "KP_Subtract"},
    { IM_VK_DECIMAL,       "KP_Decimal"},
    { IM_VK_DIVIDE,        "KP_Divide"},
    { IM_VK_DELETE,        "KP_Delete"},
    { IM_VK_NUM_LOCK,      "Num_Lock"},
    { IM_VK_SCROLL_LOCK,   "Scroll_Lock"},
    { IM_VK_F1,            "F1"},
    { IM_VK_F2,            "F2"},
    { IM_VK_F3,            "F3"},
    { IM_VK_F4,            "F4"},
    { IM_VK_F5,            "F5"},
    { IM_VK_F6,            "F6"},
    { IM_VK_F7,            "F7"},
    { IM_VK_F8,            "F8"},
    { IM_VK_F9,            "F9"},
    { IM_VK_F10,           "F10"},
    { IM_VK_F11,           "F11"},
    { IM_VK_F12,           "F12"},
    { IM_VK_F13,           "F13"},
    { IM_VK_F14,           "F14"},
    { IM_VK_F15,           "F15"},
    { IM_VK_F16,           "F16"},
    { IM_VK_F17,           "F17"},
    { IM_VK_F18,           "F18"},
    { IM_VK_F19,           "F19"},
    { IM_VK_F20,           "F20"},
    { IM_VK_F21,           "F21"},
    { IM_VK_F22,           "F22"},
    { IM_VK_F23,           "F23"},
    { IM_VK_F24,           "F24"},
    { IM_VK_PRINTSCREEN,   "PrintScreen"},
    { IM_VK_INSERT,        "Insert"},
    { IM_VK_HELP,          "Help"},
    { IM_VK_META,          "Meta_L"},
    { IM_VK_BACK_QUOTE,    "BackQuote"},
    { IM_VK_QUOTE,         "Quote"},
    { IM_VK_KP_UP,         "KP_Up"},
    { IM_VK_KP_DOWN,       "KP_Down"},
    { IM_VK_KP_LEFT,       "KP_Left"},
    { IM_VK_KP_RIGHT,      "KP_Right"},
    { IM_VK_DEAD_GRAVE,              "dead_grave"},
    { IM_VK_DEAD_ACUTE,              "dead_acute"},
    { IM_VK_DEAD_CIRCUMFLEX,         "dead_circumflex"},
    { IM_VK_DEAD_TILDE,              "dead_tilde"},
    { IM_VK_DEAD_MACRON,             "dead_macron"},
    { IM_VK_DEAD_BREVE,              "dead_breve"},
    { IM_VK_DEAD_ABOVEDOT,           "dead_abovedot"},
    { IM_VK_DEAD_DIAERESIS,          "dead_diaeresis"},
    { IM_VK_DEAD_ABOVERING,          "dead_abovering"},
    { IM_VK_DEAD_DOUBLEACUTE,        "dead_doubleacute"},
    { IM_VK_DEAD_CARON,              "dead_caron"},
    { IM_VK_DEAD_CEDILLA,            "dead_cedilla"},
    { IM_VK_DEAD_OGONEK,             "dead_ogonek"},
    { IM_VK_DEAD_IOTA,               "dead_iota"},
    { IM_VK_DEAD_VOICED_SOUND,       "dead_voiced_sound"},
    { IM_VK_DEAD_SEMIVOICED_SOUND,   "dead_semivoiced_sound"},
    { IM_VK_AMPERSAND,               "ampersand"},
    { IM_VK_ASTERISK,                "asterisk"},
    { IM_VK_QUOTEDBL,                "quotedbl"},
    { IM_VK_LESS,                    "less"},
    { IM_VK_GREATER,                 "greater"},
    { IM_VK_BRACELEFT,               "braceleft"},
    { IM_VK_BRACERIGHT,              "braceright"},
    { IM_VK_AT,                      "at"},
    { IM_VK_COLON,                   "colon"},
    { IM_VK_CIRCUMFLEX,              "circumflex"},
    { IM_VK_DOLLAR,                  "dollar"},
    { IM_VK_EURO_SIGN,               "euro"},  /* ? */
    { IM_VK_EXCLAMATION_MARK,        "exclam"},
    { IM_VK_INVERTED_EXCLAMATION_MARK,"inverted_exclam"}, /* ? */
    { IM_VK_LEFT_PARENTHESIS,        "parenleft"},
    { IM_VK_NUMBER_SIGN,             "numbersign"},
    { IM_VK_PLUS,                    "plus"},
    { IM_VK_RIGHT_PARENTHESIS,       "parenright"},
    { IM_VK_UNDERSCORE,              "underscore"},
    { IM_VK_FINAL,                   "final"},         /* ? */
    { IM_VK_CONVERT,                 "convert"},       /* ? */
    { IM_VK_NONCONVERT,              "noconvert"},       /* ? */
    { IM_VK_ACCEPT,                  "accept"},       /* ? */
    { IM_VK_MODECHANGE,              "modechange"},       /* ? */
    { IM_VK_KANA,                    "kana"},       /* ? */
    { IM_VK_FINAL,                   "final"},       /* ? */
    { IM_VK_KANJI,                   "kanji"},       /* ? */
    { IM_VK_ALPHANUMERIC,            "alphanumberic"},       /* ? */
    { IM_VK_KATAKANA,                "katakana"},       /* ? */
    { IM_VK_HIRAGANA,                "rhkiragana"},       /* ? */
    { IM_VK_FULL_WIDTH,              "fullwidth"},       /* ? */
    { IM_VK_HALF_WIDTH,              "halfwidth"},       /* ? */
    { IM_VK_ROMAN_CHARACTERS,        "romancandidates"},       /* ? */
    { IM_VK_ALL_CANDIDATES,          "allcandidates"},       /* ? */
    { IM_VK_PREVIOUS_CANDIDATE,      "previouscandidate"},       /* ? */
    { IM_VK_CODE_INPUT,              "codeinput"},       /* ? */
    { IM_VK_JAPANESE_KATAKANA,       "japanese_katakana"},       /* ? */
    { IM_VK_JAPANESE_HIRAGANA,       "japanese_hiragana"},       /* ? */
    { IM_VK_JAPANESE_ROMAN,          "japanese_roman"},       /* ? */
    { IM_VK_CUT,                     "cut"},       /* ? */
    { IM_VK_COPY,                    "copy"},       /* ? */
    { IM_VK_PASTE,                   "paste"},       /* ? */
    { IM_VK_UNDO,                    "undo"},       /* ? */
    { IM_VK_AGAIN,                   "again"},       /* ? */
    { IM_VK_FIND,                    "find"},       /* ? */
    { IM_VK_PROPS,                   "props"},       /* ? */
    { IM_VK_STOP,                    "stop"},       /* ? */
    { IM_VK_COMPOSE,                 "compose"},       /* ? */
    { IM_VK_ALT_GRAPH,               "alt_graph"}       /* ? */
};

char *iiim_keycode_to_name(short keycode)
{
    int total, i;

    total = sizeof(keycode_name_pairs)/sizeof(keycode_name_pair_t);

    for (i = 0; i < total; i++) {
        if (keycode_name_pairs[i].keycode == keycode)
            return (keycode_name_pairs[i].name);
    }

    return NULL;
}


static MSymbol
keyevent_to_msymbol(IMKeyEvent *ev)
{
	int keyChar = ev->keyChar;
	int modifier = ev->modifier;
    int keyCode = ev->keyCode;
	char key[256];
	memset(key, 0, 256);

    // ref: minput_event_to_key()

    if (keyChar >= 0x20 && keyChar <= 0x7e) {
        if (modifier & IM_SHIFT_MASK) {
            if (keyChar  >= 'a' && keyChar <= 'z') {
                keyChar += 'A' - 'a';
                modifier &= ~IM_SHIFT_MASK;
            } else if (keyChar >= 'A' && keyChar <= 'Z') {
                modifier &= ~IM_SHIFT_MASK;
            }
        }
        key[0] = keyChar;
    } else if (keyChar == 0) {
        if (keyCode == IM_VK_SHIFT ||
            keyCode == IM_VK_CONTROL ||
            keyCode == IM_VK_ALT ||
            keyCode == IM_VK_CAPS_LOCK)
            return Mnil;
        char *keystr = iiim_keycode_to_name(keyCode);
        if (!keystr)
            return Mnil;
        strncpy(key, keystr, 255);
    }
    if (!key[0])
        return Mnil;
    if (key[1] == '\0' && (modifier & IM_CTRL_MASK)) {
        // name is one byte length
        if (key[0] >= 'a' && key[0] <= 'z') {
            key[0] += 'A' - 'a';
        }
    }

	char buf[1024];
	snprintf(buf, 1023, "%s%s%s%s%s%s", 
			(modifier & IM_SHIFT_MASK)? "S-" : "",
			(modifier & IM_CTRL_MASK)?  "C-" : "",
			(modifier & IM_META_MASK)?  "M-" : "",
			(modifier & IM_ALT_MASK)?   "A-" : "",
			(modifier & IM_ALT_GRAPH_MASK)? "H-" : "",
			key);

	return msymbol(buf);
}

static const unsigned char *
utf8_next_char(const unsigned char *in)
{
    if (*in < 128)
        return (in + 1);
    else if ((*in & 0xE0) == 0xC0)
        return (in + 2);
    else if ((*in & 0xF0) == 0xE0)
        return (in + 3);
    else if ((*in & 0xF8) == 0xF0)
        return (in + 4);
    else if ((*in & 0xFC) == 0xF8)
        return (in + 5);
    else if ((*in & 0xFE) == 0xFC)
        return (in + 6);

    // invalid!
    return NULL;
}

// vi:ts=4:sw=4:nowrap:cin:expandtab
